package com.ubxtech.gateway.filter;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Method;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ubxtech.core.context.ContextConstants;
import com.ubxtech.core.context.ContextUtil;
import com.ubxtech.core.dto.LogDTO;
import com.ubxtech.core.utils.AntPathMatcherUtil;
import com.ubxtech.core.utils.SpringUtils;
import com.ubxtech.gateway.properties.CustomGatewayProperties;
import com.ubxtech.gateway.util.IPUtils;
import io.netty.util.internal.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.multipart.FormFieldPart;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import jakarta.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.*;


/**
 * https://blog.csdn.net/Instanceztt/article/details/127532676
 * @author Victor.Xiao
 * @since 2023-08-30 21:04
 **/
@Component
@Slf4j
public class GatewayAccessLogFilter implements GlobalFilter, Ordered {

    @Resource
    private CustomGatewayProperties customGatewayProperties;

    private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

    /**
     * default HttpMessageReader.
     */
    private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();


    @Override
    public int getOrder() {
        return -10;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        // 判断是否打开相应是日志配置 ingore配置校验
        if (customGatewayProperties.getLogFlag()==0||hasIgnoredFlag(path)){
            return chain.filter(exchange);
        }
        // 获得请求上下文
        LogDTO logDTO = parseLogDTO(exchange);
        MediaType mediaType = request.getHeaders().getContentType();
        if (Objects.isNull(mediaType) || Method.GET.toString().equalsIgnoreCase(logDTO.getRequestMethod())) {
            return writeNormalLog(exchange, chain, logDTO);
        }
        logDTO.setRequestContentType(mediaType.toString());
        // 对不同的请求类型做相应的处理
        if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)){
            return writeBodyLog(exchange,chain,logDTO);
        }else if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType) || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)){
            //readFormData消耗内存过多,不读取入参和出参,待解决 TODO
//            return readFormData(exchange,chain,logDTO);
            return writeNormalLog(exchange, chain, logDTO);
        }else {
            return writeBasicLog(exchange,chain,logDTO);
        }
    }

    /**
     * 是否忽略日志
     * @author Victor.Xiao
     * @since 2023-08-31 09:55
     * @param path path
     * @return java.lang.Boolean
    */
    private Boolean hasIgnoredFlag(String path){
        List<String> ignoredPatterns = customGatewayProperties.getLogExcludePath();
        if (CollectionUtil.isEmpty(ignoredPatterns)){
            return Boolean.FALSE;
        }
        return AntPathMatcherUtil.match(path,ignoredPatterns);
    }


    private LogDTO parseLogDTO(ServerWebExchange exchange){
        ServerHttpRequest request = exchange.getRequest();
        String requestPath = request.getPath().pathWithinApplication().value();

        LogDTO logDTO = new LogDTO();
        // 获取路由信息
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        String ipAddress = IPUtils.getIpAddr(request);

        logDTO.setOrigin(request.getHeaders().getFirst(ContextConstants.HEADER_ORIGIN));
        logDTO.setSchema(request.getURI().getScheme());
        logDTO.setRequestMethod(request.getMethod().name());
        logDTO.setRequestPath(requestPath);
        logDTO.setTargetServer(route != null ? route.getUri().toString() : "");
        logDTO.setRequestTime(new Date());
        logDTO.setIp(ipAddress);
        logDTO.setRouteConfig(JSON.toJSONString(route));
        Map<String, Object> headers = new HashMap<>();
        for (String key : request.getHeaders().keySet()) {
            headers.put(key, request.getHeaders().getFirst(key));
        }
        logDTO.setHeaders(JSON.toJSONString(headers));

        logDTO.setPTraceId(ContextUtil.getPTraceId());
        logDTO.setXTraceId(ContextUtil.getXTraceId());

        return logDTO;
    }

    private Mono<Void> writeNormalLog(ServerWebExchange exchange, GatewayFilterChain chain, LogDTO logDTO){
        //获取响应体
        ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, logDTO);
        return chain.filter(exchange.mutate().request(exchange.getRequest()).response(decoratedResponse).build())
                .then(Mono.fromRunnable(() -> {
                    ServerHttpResponse response = exchange.getResponse();
                    logDTO.setStatus(response.getStatusCode() == null ? "" : String.valueOf(response.getStatusCode().value()));
                    Date responseDate = new Date();
                    logDTO.setResponseTime(responseDate);
                    long executeTime = DateUtil.between(logDTO.getRequestTime(), responseDate, DateUnit.MS);
                    logDTO.setExecuteTime(executeTime);
                    ServerHttpRequest request = exchange.getRequest();
                    MultiValueMap<String, String> queryParams = request.getQueryParams();
                    Map<String, String> paramsMap = new HashMap<>();
                    if (CollectionUtil.isNotEmpty(queryParams)) {
                        for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
                            paramsMap.put(entry.getKey(), StrUtil.join(StrPool.COMMA,entry.getValue()));
                        }
                    }
                    logDTO.setRequestBody(JSONObject.toJSONString(paramsMap));

                    pubAccessLog(logDTO);
                }));

    }

    /**
     *
     * 解决 request body 只能读取一次问题，
     * 参考: org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
     * @author Victor.Xiao
     * @since 2023-08-31 10:18
     * @param exchange exchange
     * @param chain chain
     * @param logDTO logDTO
     * @return reactor.core.publisher.Mono
    */
    @SuppressWarnings("unchecked")
    private Mono<Void> writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, LogDTO logDTO) {
        ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
            logDTO.setRequestBody(body);
            return Mono.just(body);
        });
        // 通过 BodyInserter 插入 body(支持修改body), 避免 request body 只能获取一次
        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        // the new content type will be computed by bodyInserter
        // and then set in the request decorator
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
        // 重新封装请求
        return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
            // 记录响应日志
            ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
            //记录普通的
            ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, logDTO);
            return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(() -> {                                // 打印日志
                pubAccessLog(logDTO);
            }));
        }));
    }


    /**
     * 读取form-data数据
     * @author Victor.Xiao
     * @since 2023-08-31 10:22
     * @param exchange exchange
     * @param chain chain
     * @param logDTO logDTO
     * @return reactor.core.publisher.Mono<java.lang.Void>
    */
    private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, LogDTO logDTO) {
        return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
            DataBufferUtils.retain(dataBuffer);
            final Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
            final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux<DataBuffer> getBody() {
                    return cachedFlux;
                }
                @Override
                public MultiValueMap<String, String> getQueryParams() {
                    return UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build().getQueryParams();
                }
            };
            final HttpHeaders headers = exchange.getRequest().getHeaders();
            if (headers.getContentLength() == 0) {
                return chain.filter(exchange);
            }
            ResolvableType resolvableType;
            if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(headers.getContentType())) {
                resolvableType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
            } else {
                //解析 application/x-www-form-urlencoded
                resolvableType = ResolvableType.forClass(String.class);
            }

            return MESSAGE_READERS.stream().filter(reader -> reader.canRead(resolvableType, mutatedRequest.getHeaders().getContentType())).findFirst().orElseThrow(() -> new IllegalStateException("no suitable HttpMessageReader.")).readMono(resolvableType, mutatedRequest, Collections.emptyMap()).flatMap(resolvedBody -> {
                if (resolvedBody instanceof MultiValueMap) {
                    LinkedMultiValueMap map = (LinkedMultiValueMap) resolvedBody;
                    if (CollectionUtil.isNotEmpty(map)) {
                        StringBuilder builder = new StringBuilder();
                        final Part bodyPartInfo = (Part) ((MultiValueMap) resolvedBody).getFirst("body");
                        if (bodyPartInfo instanceof FormFieldPart) {
                            String body = ((FormFieldPart) bodyPartInfo).value();
                            builder.append("body=").append(body);
                        }
                        logDTO.setRequestBody(builder.toString());
                    }
                } else {
                    logDTO.setRequestBody((String) resolvedBody);
                }

                //获取响应体
                ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, logDTO);
                return chain.filter(exchange.mutate().request(mutatedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(() -> {                                    // 打印日志
                    pubAccessLog(logDTO);
                }));
            });
        });
    }


    private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, LogDTO logDTO) {
        return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
            DataBufferUtils.retain(dataBuffer);
            final Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
            final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux<DataBuffer> getBody() {
                    return cachedFlux;
                }

                @Override
                public MultiValueMap<String, String> getQueryParams() {
                    return UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build().getQueryParams();
                }
            };
            StringBuilder builder = new StringBuilder();
            MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
            if (CollectionUtil.isNotEmpty(queryParams)) {
                for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
                    builder.append(entry.getKey()).append("=").append(entry.getValue()).append(StrPool.COMMA);
                }
            }
            //获取响应体
            logDTO.setRequestBody(builder.toString());
            ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, logDTO);
            return chain.filter(exchange.mutate().request(mutatedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(() -> {                        // 打印日志
               pubAccessLog(logDTO);
            }));
        });
    }

    /**
     * 请求装饰器，重新计算 headers
     * @param exchange
     * @param headers
     * @param outputMessage
     * @return
     */
    private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                if (contentLength > 0) {
                    httpHeaders.setContentLength(contentLength);
                } else {
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }
            @Override
            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
    }

    /**
     * 记录响应日志
     * 通过 DataBufferFactory 解决响应体分段传输问题。
     */
    private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, LogDTO logDTO) {
        ServerHttpResponse response = exchange.getResponse();
        NettyDataBufferFactory bufferFactory = (NettyDataBufferFactory)response.bufferFactory();
        return new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Date responseDate = new Date();
                    logDTO.setResponseTime(responseDate);
                    // 计算执行时间
                    long executeTime = DateUtil.between(logDTO.getRequestTime(), responseDate, DateUnit.MS);
                    logDTO.setExecuteTime(executeTime);
                    // 获取响应类型，如果是 json 就打印
                    String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
                    logDTO.setStatus(this.getStatusCode() == null ? "" : String.valueOf(this.getStatusCode().value()));
                    //
                    if (!StringUtil.isNullOrEmpty(originalResponseContentType)
                            && originalResponseContentType.contains("application/json")) {
                        Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                        return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                            // 合并多个流集合，解决返回体分段传输,用NettyDataBuffer才可以被DataBufferUtils.release释放
//                            DataBufferFactory dataBufferFactory = new NettyDataBufferFactory();
                            NettyDataBuffer join = (NettyDataBuffer)bufferFactory.join(dataBuffers);

                            byte[] content = new byte[join.readableByteCount()];
                            // 释放掉内存
                            join.read(content);
                            DataBufferUtils.release(join);
                            String responseResult = new String(content, StandardCharsets.UTF_8);
                            logDTO.setResponseData(responseResult);
                            return bufferFactory.wrap(content);
                        }));
                    }
                }
                return super.writeWith(body);
            }
        };
    }



    /**
     * 打印网关日志
     */
    private void pubAccessLog(LogDTO logDTO) {

        log.info("api请求结果:[{}]", logDTO);
        SpringUtils.publishEvent(logDTO);
    }


}

